#!/usr/bin/env python3

# Copyright (c) 2005-2014 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import glob
import shutil
from ._util import rel_path, compare_files, compare_directories
from ._const import PlatformType
from ._errors import SourceError, CopySourceError

import os
import glob
import shutil
import struct
import parted
import pathlib
import reedsolo
from ._util import rel_path, force_rm, force_mkdir, rmdir_if_empty, compare_file_and_content, compare_files, compare_directories, is_buffer_all_zero, PartiUtil
from ._const import TargetAccessMode, PlatformType, PlatformInstallInfo
from ._errors import TargetError, InstallError, CompareWithSourceError
from ._handy import Handy, Grub, GrubMountPoint
from ._source import Source


class Source:

    CAP_NLS = 1
    CAP_FONTS = 2
    CAP_THEMES = 3

    def __init__(self, base_dir=None):
        if base_dir is not None:
            self._baseDir = base_dir
        else:
            self._baseDir = "/"
        self._libDir = os.path.join(self._baseDir, "usr", "lib", "grub")
        self._shareDir = os.path.join(self._baseDir, "usr", "share", "grub")
        self._localeDir = os.path.join(self._baseDir, "usr", "share", "locale")
        self._themesDir = os.path.join(self._baseDir, "usr", "share", "grub", "themes")

        # check
        if not os.path.isdir(self._libDir):
            raise SourceError("directory %s does not exist" % (self._libDir))
        if not os.path.isdir(self._shareDir):
            raise SourceError("directory %s does not exist" % (self._shareDir))
        self.get_all_platform_directories()

    def supports(self, key):
        if key == self.CAP_NLS:
            return os.path.exists(self._localeDir)
        elif key == self.CAP_FONTS:
            return len(glob.glob(os.path.join(self._shareDir, "*.pf2"))) > 0
        elif key == self.CAP_THEMES:
            return os.path.exists(self._themesDir)
        else:
            assert False

    def get_all_platform_directories(self):
        ret = dict()
        for fullfn in glob.glob(os.path.join(self._libDir, "*")):
            n = os.path.basename(fullfn)
            try:
                ret[PlatformType(n)] = fullfn
            except ValueError:
                raise SourceError("invalid platform directory %s" % (fullfn))
        return ret

    def get_platform_directory(self, platform_type):
        ret = self.try_get_platform_directory(platform_type)
        assert ret is not None
        return ret

    def try_get_platform_directory(self, platform_type):
        assert isinstance(platform_type, PlatformType)
        ret = os.path.join(self._libDir, platform_type.value)
        if os.path.exists(ret):
            assert os.path.isdir(ret)
            return ret
        else:
            return None

    def get_all_locale_files(self):
        assert self.supports(self.CAP_NLS)
        ret = dict()
        for fullfn in glob.glob(os.path.join(self._localeDir, "**/LC_MESSAGES/grub.mo")):
            n = rel_path(self._localeDir, fullfn).split("/")[0]
            ret[n] = fullfn
        return ret

    def get_locale_file(self, locale_name):
        ret = self.try_get_locale_file(locale_name)
        assert ret is not None
        return ret

    def try_get_locale_file(self, locale_name):
        assert self.supports(self.CAP_NLS)
        ret = os.path.join(self._localeDir, locale_name, "LC_MESSAGES", "grub.mo")
        if os.path.exists(ret):
            assert os.path.isfile(ret)
            return ret
        else:
            return None

    def get_all_font_files(self):
        assert self.supports(self.CAP_FONTS)
        ret = dict()
        for fullfn in glob.glob(os.path.join(self._shareDir, "*.pf2")):
            n = os.path.basename(fullfn).replace(".pf2", "")
            ret[n] = fullfn
        return ret

    def get_font_file(self, font_name):
        ret = self.try_get_font_file(font_name)
        assert ret is not None
        return ret

    def try_get_font_file(self, font_name):
        assert self.supports(self.CAP_FONTS)
        ret = os.path.join(self._shareDir, font_name + ".pf2")
        if os.path.exists(ret):
            assert os.path.isfile(ret)
            return ret
        else:
            return None

    def get_default_font(self):
        assert self.supports(self.CAP_FONTS)
        return "unicode"

    def get_all_theme_directories(self):
        assert self.supports(self.CAP_THEMES)
        ret = dict()
        for fullfn in glob.glob(os.path.join(self._themesDir, "*")):
            n = os.path.basename(fullfn)
            ret[n] = fullfn
        return ret

    def get_theme_directory(self, theme_name):
        ret = self.try_get_theme_directory(theme_name)
        assert ret is not None
        return ret

    def try_get_theme_directory(self, theme_name):
        assert self.supports(self.CAP_THEMES)
        ret = os.path.join(self._themesDir, theme_name)
        if os.path.exists(ret):
            assert os.path.isdir(ret)
            return ret
        else:
            return None

    def get_default_theme(self):
        assert self.supports(self.CAP_THEMES)
        return "starfield"

    def copy_into(self, dest_dir):
        assert os.path.isdir(dest_dir)

        # copy platform directories
        tdir = os.path.join(dest_dir, rel_path(self._baseDir, self._libDir))
        os.makedirs(tdir, exist_ok=True)
        for fullfn in self.get_all_platform_directories().values():
            fullfn2 = os.path.join(tdir, rel_path(self._libDir, fullfn))
            if os.path.exists(fullfn2):
                if not compare_directories(fullfn, fullfn2):
                    raise CopySourceError("%s and %s are different" % (fullfn, fullfn2))
            else:
                shutil.copytree(fullfn, fullfn2)

        # copy locale files
        if self.supports(self.CAP_NLS):
            tdir = os.path.join(dest_dir, rel_path(self._baseDir, self._localeDir))
            os.makedirs(tdir, exist_ok=True)
            for fullfn in self.get_all_locale_files().values():
                fullfn2 = os.path.join(tdir, rel_path(self._localeDir, fullfn))
                if os.path.exists(fullfn2):
                    if not compare_files(fullfn, fullfn2):
                        raise CopySourceError("%s and %s are different" % (fullfn, fullfn2))
                else:
                    os.makedirs(os.path.dirname(fullfn2), exist_ok=True)
                    shutil.copy(fullfn, fullfn2)

        # copy font files
        if self.supports(self.CAP_FONTS):
            tdir = os.path.join(dest_dir, rel_path(self._baseDir, self._shareDir))
            os.makedirs(tdir, exist_ok=True)
            for fullfn in self.get_all_font_files().values():
                fullfn2 = os.path.join(tdir, rel_path(self._shareDir, fullfn))
                if os.path.exists(fullfn2):
                    if not compare_files(fullfn, fullfn2):
                        raise CopySourceError("%s and %s are different" % (fullfn, fullfn2))
                else:
                    shutil.copy(fullfn, fullfn2)

        # copy theme directories
        if self.supports(self.CAP_THEMES):
            tdir = os.path.join(dest_dir, rel_path(self._baseDir, self._themesDir))
            os.makedirs(tdir, exist_ok=True)
            for fullfn in self.get_all_theme_directories().values():
                fullfn2 = os.path.join(tdir, rel_path(self._themesDir, fullfn))
                if os.path.exists(fullfn2):
                    if not compare_directories(fullfn, fullfn2):
                        raise CopySourceError("%s and %s are different" % (fullfn, fullfn2))
                else:
                    shutil.copytree(fullfn, fullfn2)


class Target:

    def __init__(self, target_access_mode, **kwargs):
        assert isinstance(target_access_mode, TargetAccessMode)

        self._mode = target_access_mode
        self._tmpDir = kwargs.get("tmp_work_dir", None)

        # target specific variables
        rootfsMnt = kwargs.get("rootfs_mount_point", None)
        bootMnt = kwargs.get("boot_mount_point", None)
        if bootMnt is not None:
            if rootfsMnt is not None:
                assert os.path.join(rootfsMnt.mountpoint, "boot") == bootMnt.mountpoint
            self._mnt = GrubMountPoint(bootMnt, False)
            self._bootDir = self._mnt.mountpoint
        elif rootfsMnt is not None:
            self._mnt = GrubMountPoint(rootfsMnt, True)
            self._bootDir = os.path.join(self._mnt.mountpoint, "boot")
            if not os.path.exists(self._bootDir):
                raise TargetError("boot directory \"%s\" does not exist" % (self._bootDir))
        else:
            assert False
        if self._mnt.fs_uuid is None:
            raise TargetError("no fsuuid found")

        # fill self._platforms
        self._platforms = dict()
        if self._mode in [TargetAccessMode.R, TargetAccessMode.RW]:
            _Common.init_platforms(self)
            for k, v in self._platforms.items():
                try:
                    if k == PlatformType.I386_PC:
                        _Bios.fill_platform_install_info_with_mbr(k, v, self._bootDir, self._mnt.disk)
                    elif Handy.isPlatformEfi(k):
                        _Efi.fill_platform_install_info(k, v, self._bootDir)
                    else:
                        assert False
                except TargetError as e:
                    self._platforms[k] = _newNotValidPlatformInstallInfo(str(e))

    @property
    def target_access_mode(self):
        return self._mode

    @property
    def platforms(self):
        return [k for k, v in self._platforms.items() if v.status == PlatformInstallInfo.Status.NORMAL]

    def get_platform_install_info(self, platform_type):
        assert isinstance(platform_type, PlatformType)

        if platform_type in self._platforms:
            return self._platforms[platform_type]
        else:
            return _newNotInstalledPlatformInstallInfo()

    def install_platform(self, platform_type, source, **kwargs):
        assert self._mode in [TargetAccessMode.RW, TargetAccessMode.W]
        assert isinstance(platform_type, PlatformType)
        assert isinstance(source, Source)

        ret = PlatformInstallInfo()
        ret.status = PlatformInstallInfo.Status.NORMAL

        if platform_type == PlatformType.I386_PC:
            _Common.install_platform(self, platform_type, source,
                                        tmpDir=self._tmpDir,
                                        debugImage=kwargs.get("debug_image", None))
            _Bios.install_with_mbr(platform_type, ret, source, self._bootDir, self._mnt.disk,
                                    False,                                                           # bFloppyOrHdd
                                    kwargs.get("allow_floppy", False),                               # bAllowFloppy
                                    kwargs.get("bpb", True),                                         # bBpb
                                    kwargs.get("rs_codes", True))                                    # bAddRsCodes
        elif Handy.isPlatformEfi(platform_type):
            assert self._mnt.is_boot_mount_point()
            if self._mnt.grub_fs != "fat":
                raise InstallError("%s must be fat filesystem" % (self._mnt.mountpoint))
            _Common.install_platform(self, platform_type, source,
                                        tmpDir=self._tmpDir,
                                        debugImage=kwargs.get("debug_image", None))
            _Efi.install_info_efi_dir(platform_type, ret, self._bootDir,
                                        kwargs.get("removable", False),                               # bRemovable
                                        kwargs.get("update_nvram", True))                             # bUpdateNvram
        else:
            assert False

        self._platforms[platform_type] = ret

    def remove_platform(self, platform_type):
        assert self._mode in [TargetAccessMode.RW, TargetAccessMode.W]
        assert isinstance(platform_type, PlatformType)

        # do nothing if the specified platform does not exists
        if platform_type not in self._platforms:
            return

        # do remove
        if platform_type == PlatformType.I386_PC:
            _Bios.remove_from_mbr(platform_type, self._mnt.disk)
        elif Handy.isPlatformEfi(platform_type):
            _Efi.remove_from_efi_dir(platform_type, self._bootDir)
        else:
            assert False
        _Common.remove_platform(self, platform_type)

        # delete PlatformInstallInfo object
        del self._platforms[platform_type]

    def install_data_files(self, source, locales=None, fonts=None, themes=None):
        assert self._mode in [TargetAccessMode.RW, TargetAccessMode.W]
        if locales is not None:
            assert source.supports(source.CAP_NLS)
        if fonts is not None:
            assert source.supports(source.CAP_FONTS)
        if themes is not None:
            assert source.supports(source.CAP_THEMES)

        grubDir = os.path.join(self._bootDir, "grub")
        force_mkdir(grubDir)

        if locales is not None:
            dstDir = os.path.join(grubDir, "locale")
            force_mkdir(dstDir, clear=True)
            if locales == "*":
                for lname, fullfn in source.get_all_locale_files().items():
                    shutil.copy(fullfn, os.path.join(dstDir, "%s.mo" % (lname)))
            else:
                for lname in locales:
                    shutil.copy(source.get_locale_file(lname), "%s.mo" % (lname))

        if fonts is not None:
            dstDir = os.path.join(grubDir, "fonts")
            force_mkdir(dstDir, clear=True)
            if fonts == "*":
                for fname, fullfn in source.get_all_font_files().items():
                    shutil.copy(fullfn, dstDir)
            else:
                for fname in fonts:
                    shutil.copy(source.get_font_file(fname), dstDir)

        if themes is not None:
            dstDir = os.path.join(grubDir, "themes")
            force_mkdir(dstDir, clear=True)
            if themes == "*":
                for tname, fullfn in source.get_all_theme_directories().items():
                    shutil.copytree(fullfn, os.path.join(dstDir, tname))
            else:
                for tname in themes:
                    shutil.copytree(source.get_theme_directory(tname), dstDir)

    def remove_data_files(self):
        assert self._mode in [TargetAccessMode.RW, TargetAccessMode.W]

        grubDir = os.path.join(self._bootDir, "grub")
        force_rm(os.path.join(grubDir, "locale"))
        force_rm(os.path.join(grubDir, "fonts"))
        force_rm(os.path.join(grubDir, "themes"))

    def remove_all(self):
        assert self._mode in [TargetAccessMode.RW, TargetAccessMode.W]

        # remove platforms, some platform needs special processing
        for k in list(self._platforms.keys()):
            self.remove_platform(k)

        # remove remaining files
        _Efi.remove_remaining_crufts(self._bootDir)
        _Common.remove_remaining_crufts(self)

    def compare_with_source(self, source):
        assert self._mode in [TargetAccessMode.R, TargetAccessMode.RW]
        assert isinstance(source, Source)

        for pt in self._platforms:
            restFiles = _Common.check_platform(self, pt, source, tmpDir=self._tmpDir)
            if pt == PlatformType.I386_PC:
                _Bios.check_rest_files(pt, source, self._bootDir, restFiles)
            elif Handy.isPlatformEfi(pt):
                pass
            else:
                assert False

        _Common.check_data(self, source)



class _Bios:

    @classmethod
    def fill_platform_install_info_without_mbr(cls, platform_type, platform_install_info, bootDir):
        cls._checkAndReadBootImg(platform_type, bootDir, TargetError)
        cls._checkAndReadCoreImg(platform_type, bootDir, TargetError)

        platform_install_info.mbr_installed = False
        platform_install_info.allow_floppy = True
        platform_install_info.bpb = True
        platform_install_info.rs_codes = True

    @classmethod
    def fill_platform_install_info_with_mbr(cls, platform_type, platform_install_info, bootDir, dev):
        bootBuf = bytearray(cls._checkAndReadBootImg(platform_type, bootDir, TargetError))     # bootBuf needs to be writable
        coreBuf = cls._checkAndReadCoreImg(platform_type, bootDir, TargetError)

        # read MBR and MBR-gap
        tmpBootBuf, tmpRestBuf = None, None
        cls._checkDisk(dev, TargetError)
        with open(dev, "rb") as f:
            tmpBootBuf = f.read(len(bootBuf))
            tmpRestBuf = f.read(cls._getCoreBufMaxSize() - len(bootBuf))

        # boot.img and core.img is not installed
        if tmpBootBuf == cls._getAllZeroBootBuf(tmpBootBuf) and is_buffer_all_zero(tmpRestBuf):
            raise TargetError("boot.img and core.img are not installed to disk")

        # compare boot.img
        if True:
            # see comment in cls.install_into_mbr()
            s, e = Grub.BOOT_MACHINE_BPB_START, Grub.BOOT_MACHINE_BPB_END
            if not is_buffer_all_zero(tmpBootBuf[s:e]):
                bootBuf[s:e] = tmpBootBuf[s:e]
                bBpb = True
            else:
                bBpb = False

            # see comment in cls.install_into_mbr()
            s, e = Grub.BOOT_MACHINE_DRIVE_CHECK, Grub.BOOT_MACHINE_DRIVE_CHECK + 2
            if tmpBootBuf[s:e] == b'\x90\x90':
                bootBuf[s:e] = tmpBootBuf[s:e]
                bAllowFloppy = False
            else:
                bAllowFloppy = True

            # see comment in cls.install_into_mbr()
            s, e = Grub.BOOT_MACHINE_WINDOWS_NT_MAGIC, Grub.BOOT_MACHINE_PART_END
            bootBuf[s:e] = tmpBootBuf[s:e]

            # do compare
            if tmpBootBuf != bootBuf:
                raise TargetError("invalid MBR record content")

        # compare core.img
        if tmpRestBuf[:len(coreBuf)] == coreBuf:
            bRsCodes = False
        else:
            coreBuf = cls._getRsEncodedCoreBuf(coreBuf, Handy.isPlatformBigEndianOrLittleEndian(platform_type))
            if tmpRestBuf[:len(coreBuf)] == coreBuf:
                bRsCodes = True
            else:
                raise TargetError("invalid on-disk core.img content")

        # compare rest bytes
        if not is_buffer_all_zero(tmpRestBuf):
            raise TargetError("disk content after core.img should be all zero")

        # return
        platform_install_info.mbr_installed = True
        platform_install_info.allow_floppy = bAllowFloppy
        platform_install_info.bpb = bBpb
        platform_install_info.rs_codes = bRsCodes

    @classmethod
    def install_without_mbr(cls, platform_type, platform_install_info, source, bootDir):
        # copy boot.img
        shutil.copy(os.path.join(source.get_platform_directory(platform_type), "boot.img"), os.path.join(bootDir, "grub", platform_type.value))

        # fill custom attributes
        platform_install_info.mbr_installed = False
        platform_install_info.allow_floppy = True
        platform_install_info.bpb = True
        platform_install_info.rs_codes = False

    @classmethod
    def install_with_mbr(cls, platform_type, platform_install_info, source, bootDir, dev, bFloppyOrHdd, bAllowFloppy, bBpb, bAddRsCodes):
        assert not bFloppyOrHdd and not bAllowFloppy        # FIXME

        # copy boot.img
        shutil.copy(os.path.join(source.get_platform_directory(platform_type), "boot.img"), os.path.join(bootDir, "grub", platform_type.value))

        bootBuf = bytearray(cls._checkAndReadBootImg(platform_type, bootDir, InstallError))     # bootBuf needs to be writable
        coreBuf = cls._checkAndReadCoreImg(platform_type, bootDir, InstallError)
        cls._checkDisk(dev, InstallError)

        with open(dev, "rb+") as f:
            tmpBootBuf = f.read(len(bootBuf))

            # prepare bootBuf
            if True:
                # Copy the possible DOS BPB.
                if bBpb:
                    s, e = Grub.BOOT_MACHINE_BPB_START, Grub.BOOT_MACHINE_BPB_END
                    bootBuf[s:e] = tmpBootBuf[s:e]

                # If DEST_DRIVE is a hard disk, enable the workaround, which is
                # for buggy BIOSes which don't pass boot drive correctly. Instead,
                # they pass 0x00 or 0x01 even when booted from 0x80.
                if not bAllowFloppy and not bFloppyOrHdd:
                    # Replace the jmp (2 bytes) with double nop's.
                    s, e = Grub.BOOT_MACHINE_DRIVE_CHECK, Grub.BOOT_MACHINE_DRIVE_CHECK + 2
                    bootBuf[s:e] == b'\x90\x90'

                # Copy the partition table.
                if not bAllowFloppy and not bFloppyOrHdd:
                    s, e = Grub.BOOT_MACHINE_WINDOWS_NT_MAGIC, Grub.BOOT_MACHINE_PART_END
                    bootBuf[s:e] = tmpBootBuf[s:e]

            # prepare coreBuf
            if bAddRsCodes:
                coreBuf = cls._getRsEncodedCoreBuf(coreBuf, Handy.isPlatformBigEndianOrLittleEndian(platform_type))

            # write up to cls._getCoreImgMaxSize()
            f.seek(0)
            f.write(bootBuf)
            f.write(coreBuf)
            for i in range(0, cls._getCoreBufMaxSize() - len(coreBuf) - len(bootBuf)):
                f.write(b'\x00')

        # fill custom attributes
        platform_install_info.mbr_installed = True
        platform_install_info.allow_floppy = bAllowFloppy
        platform_install_info.bpb = bBpb
        platform_install_info.rs_codes = bAddRsCodes

    @classmethod
    def remove_from_mbr(cls, platform_type, dev):
        cls._checkDisk(dev, None)

        with open(dev, "rb+") as f:
            # prepare allZeroBootBuf
            tmpBootBuf = f.read(Grub.DISK_SECTOR_SIZE)
            allZeroBootBuf = cls._getAllZeroBootBuf(tmpBootBuf)

            # write up to cls._getCoreImgMaxSize()
            f.seek(0)
            f.write(allZeroBootBuf)
            for i in range(0, cls._getCoreBufMaxSize() - len(allZeroBootBuf)):
                f.write(b'\x00')

    @staticmethod
    def check_rest_files(platform_type, source, bootDir, rest_files):
        srcFile = os.path.join(source.get_platform_directory(platform_type), "boot.img")
        assert os.path.exists(srcFile)

        dstFile = os.path.join(bootDir, "grub", platform_type.value, "boot.img")
        if os.path.exists(dstFile):
            assert dstFile in rest_files
            rest_files.remove(dstFile)
            if not compare_files(srcFile, dstFile):
                raise CompareWithSourceError("%s and %s are different" % (srcFile, dstFile))
        else:
            raise CompareWithSourceError("%s does not exist" % (dstFile))

        if len(rest_files) > 0:
            raise CompareWithSourceError("redundant file %s found" % (rest_files[0]))

    @staticmethod
    def _getCoreBufMaxSize():
        return Grub.DISK_SECTOR_SIZE * 1024

    @staticmethod
    def _getCoreBufPossibleSize(coreBuf):
        return (len(coreBuf) + Grub.DISK_SECTOR_SIZE - 1) // Grub.DISK_SECTOR_SIZE * Grub.DISK_SECTOR_SIZE * 2

    @classmethod
    def _checkDisk(cls, dev, exceptionClass):
        if not PartiUtil.isDiskOrParti(dev):
            if exceptionClass is not None:
                raise exceptionClass("'%s' must be a disk" % (dev))
            else:
                assert False

        pDev = parted.getDevice(dev)
        pDisk = parted.newDisk(pDev)
        if pDisk.type != "msdos":
            if exceptionClass is not None:
                raise exceptionClass("'%s' must have a MBR partition table" % (dev))
            else:
                assert False
        pPartiList = pDisk.getPrimaryPartitions()
        if len(pPartiList) == 0:
            if exceptionClass is not None:
                raise exceptionClass("'%s' have no partition" % (dev))
            else:
                assert False
        if pPartiList[0].geometry.start * pDev.sectorSize < cls._getCoreBufMaxSize():
            if exceptionClass is not None:
                raise exceptionClass("'%s' has no MBR gap or its MBR gap is too small" % (dev))
            else:
                assert False

    @staticmethod
    def _checkAndReadBootImg(platform_type, bootDir, exceptionClass):
        bootImgFile = os.path.join(bootDir, "grub", platform_type.value, "boot.img")
        if not os.path.exists(bootImgFile):
            raise exceptionClass("'%s' does not exist" % (bootImgFile))
        bootBuf = pathlib.Path(bootImgFile).read_bytes()
        if len(bootBuf) != Grub.DISK_SECTOR_SIZE:
            raise exceptionClass("the size of '%s' is not %u" % (bootImgFile, Grub.DISK_SECTOR_SIZE))
        return bootBuf

    @classmethod
    def _checkAndReadCoreImg(cls, platform_type, bootDir, exceptionClass):
        coreImgFile = os.path.join(bootDir, "grub", platform_type.value, Grub.getCoreImgNameAndTarget(platform_type)[0])
        if not os.path.exists(coreImgFile):
            raise exceptionClass("'%s' does not exist" % (coreImgFile))
        coreBuf = pathlib.Path(coreImgFile).read_bytes()
        if not (Grub.DISK_SECTOR_SIZE <= cls._getCoreBufPossibleSize(coreBuf) <= cls._getCoreBufMaxSize()):
            raise exceptionClass("the size of '%s' is invalid" % (coreImgFile))
        return coreBuf

    @staticmethod
    def _getAllZeroBootBuf(onDiskBootBuf):
        allZeroBootBuf = bytearray(Grub.DISK_SECTOR_SIZE - 2) + b'\x55\xAA'

        # see comment in cls.install_into_mbr()
        s, e = Grub.BOOT_MACHINE_BPB_START, Grub.BOOT_MACHINE_BPB_END
        allZeroBootBuf[s:e] = onDiskBootBuf[s:e]

        # see comment in cls.install_into_mbr()
        s, e = Grub.BOOT_MACHINE_WINDOWS_NT_MAGIC, Grub.BOOT_MACHINE_PART_END
        allZeroBootBuf[s:e] = onDiskBootBuf[s:e]

        return bytes(allZeroBootBuf)

    @classmethod
    def _getRsEncodedCoreBuf(cls, coreBuf, bigOrLittleEndian):
        noRsLen = struct.unpack_from(">H" if bigOrLittleEndian else "<H",
                                     coreBuf, Grub.DISK_SECTOR_SIZE + Grub.KERNEL_I386_PC_NO_REED_SOLOMON_LENGTH)[0]
        if noRsLen == 0xFFFF:
            raise InstallError("core.img version mismatch")

        newLen = cls._getCoreBufPossibleSize(coreBuf)
        coreBuf = bytearray(coreBuf)
        struct.pack_into(">I" if bigOrLittleEndian else "<I",
                         coreBuf, Grub.DISK_SECTOR_SIZE + Grub.KERNEL_I386_PC_REED_SOLOMON_REDUNDANCY, newLen)

        noRsLen += Grub.DISK_SECTOR_SIZE
        rsc = reedsolo.RSCodec(newLen - len(coreBuf))
        return bytes(coreBuf[:noRsLen]) + rsc.encode(coreBuf[noRsLen:])


class _Efi:

    """We only support removable, and not upgrading NVRAM"""

    @staticmethod
    def fill_platform_install_info(platform_type, platform_install_info, bootDir):
        efiDir = os.path.join(bootDir, "EFI")
        coreFullfn = os.path.join(bootDir, "grub", platform_type.value, Grub.getCoreImgNameAndTarget(platform_type)[0])
        efiFullfn = os.path.join(efiDir, "BOOT", Handy.getStandardEfiFilename(platform_type))

        if not os.path.exists(coreFullfn):
            raise TargetError("%s does not exist" % (coreFullfn))
        if not os.path.exists(efiFullfn):
            raise TargetError("%s does not exist" % (efiFullfn))
        if not compare_files(coreFullfn, efiFullfn):
            raise TargetError("%s and %s are different" % (coreFullfn, efiFullfn))

        platform_install_info.removable = True
        platform_install_info.nvram = False

    @staticmethod
    def install_info_efi_dir(platform_type, platform_install_info, bootDir, bRemovable, bUpdateNvram):
        assert bRemovable and not bUpdateNvram          # FIXME

        grubPlatDir = os.path.join(bootDir, "grub", platform_type.value)
        efiDir = os.path.join(bootDir, "EFI")
        efiDirLv2 = os.path.join(efiDir, "BOOT")
        efiFn = Handy.getStandardEfiFilename(platform_type)

        # create efi dir
        force_mkdir(efiDir)

        # create level 2 efi dir
        force_mkdir(efiDirLv2)

        # copy efi file
        coreName = Grub.getCoreImgNameAndTarget(platform_type)[0]
        shutil.copy(os.path.join(grubPlatDir, coreName), os.path.join(efiDirLv2, efiFn))

        # fill custom attributes
        platform_install_info.removable = bRemovable
        platform_install_info.nvram = bUpdateNvram

    @staticmethod
    def remove_from_efi_dir(platform_type, bootDir):
        efiFn = Handy.getStandardEfiFilename(platform_type)
        for efiDir in [os.path.join(bootDir, "EFI")]:
            efiDirLv2 = os.path.join(efiDir, "BOOT")
            force_rm(os.path.join(efiDirLv2, efiFn))
            rmdir_if_empty(efiDirLv2)
            rmdir_if_empty(efiDir)

    @staticmethod
    def remove_remaining_crufts(bootDir):
        force_rm(os.path.join(bootDir, "EFI"))
